Skip to content

feat(connectors): callApi for metered connectors - #256

Merged
ChenMachBase merged 5 commits into
mainfrom
connectors-money
Aug 26, 2026
Merged

feat(connectors): callApi for metered connectors#256
ChenMachBase merged 5 commits into
mainfrom
connectors-money

Conversation

@ChenMachBase

Copy link
Copy Markdown
Contributor

Summary

Adds callApi for metered connectors — connectors backed by paid third-party APIs that charge Base44 per call.

For those, the OAuth token is deliberately unavailable to app code (getConnection rejects with a 403), because the Base44 proxy is the only place calls we pay for can be counted. callApi is the replacement:

const res = await base44.asServiceRole.connectors.callApi('x', {
  method: 'POST',
  path: '/2/tweets',
  body: { text: 'Shipped!' },
});
// -> { success, status, data, headers, creditsCharged }

Only platform connectors are affected. A workspace-registered or app user connector runs on your own OAuth app, so the provider invoices you directly and there is nothing for Base44 to meter — those keep normal token access via getWorkspaceConnection() / getCurrentAppUserConnection(). That is why there is one method here and not three.

Two shape decisions worth reviewing

An upstream 4xx/5xx resolves, it does not throw. A provider error is a normal outcome of a call Base44 completed and billed, so it comes back as success: false with the provider's own status and data. Only Base44-side failures — no connection, credits exhausted, a rejected request — reject the promise. Tests pin both sides of that split.

query is always forwarded, never dropped. The server prices the merged query string, so an SDK that accepted the field and then discarded it would make the quoted price and the real request disagree. (This is the client-side half of a billing bypass caught during review of the backend change.)

Responses carry creditsCharged, and the module docs gain a "Metered connectors" section noting that cost can vary by two orders of magnitude between endpoints on the same connector — an expensive call inside a loop being the failure mode worth warning about.

Testing

  • 9 new unit tests in tests/unit/connectors-proxy.test.ts: payload normalization, snake_casecamelCase mapping, query forwarding, upstream-error-resolves vs Base44-error-rejects, and the metered 403 surfacing an actionable message.
  • Full suite green: 210 tests, plus tsc --noEmit, npm run test:types, and eslint src.
  • Installed with npm ci (lockfile-exact). Up to date with main.

Notes

  • No version bumppackage.json is at main's 0.8.42. Publishing, and bumping the pinned @base44/sdk in the platform's builder prompts, is a separate release step; that pin touches several prompt files including system-prompt ones.
  • Backend counterpart: base44-dev/apper#19753. It ships inert (no connector is registered as metered yet), so nothing here is load-bearing until it is.
  • Supersedes feat(connectors): callApi for metered connectors #250, which was opened from a fork before I had write access here — fork PRs get no CI and cannot run preview-publish, so this replaces it.

ChenMachBase and others added 3 commits August 12, 2026 12:33
Some connectors are backed by paid third-party APIs that charge Base44 per call.
For those the OAuth token is not available to app code — getConnection and its
siblings reject with a 403 — because the Base44 proxy is the only place those
calls can be counted. Adds the three proxy methods that replace them:

- callApi(integrationType, request)          — shared platform connector
- callWorkspaceApi(connectorId, request)     — workspace-registered connector
- callCurrentAppUserApi(connectorId, request) — per-app-user connector

Each mirrors its getConnection counterpart, so the identifier you already use
carries over.

Two deliberate shape decisions:

- An upstream 4xx/5xx resolves with `success: false` and the provider's own
  `status`/`data` rather than throwing. It is a normal outcome of a call Base44
  completed and billed; only Base44-side failures (no connection, credits
  exhausted, a rejected request) reject.
- `query` is always sent, never dropped. The server prices the merged query
  string, so a client that accepted the field and then discarded it would make
  the quoted price and the real request disagree.

Responses carry `creditsCharged` so callers can see what a call actually cost,
and the module docs call out that cost varies sharply by endpoint — an expensive
call inside a loop is the failure mode worth warning about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops callWorkspaceApi and callCurrentAppUserApi. They implied that
workspace-registered and app user connectors can be metered, and they can't: both
run on the workspace's *own* OAuth app, so the provider invoices the workspace
directly. Proxying them would have billed the customer credits on top of a vendor
bill they already pay.

Only a platform connector runs on Base44's OAuth app, so callApi is the only one
of the three that ever had something to meter. The backend's matching routes are
gone too (base44-dev/apper#19753).

The module docs now say which connectors this applies to and, more usefully, why
the other two don't — so the next person doesn't re-add the methods on the
assumption they were an oversight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ChenMachBase's task —— View job


Code Review: callApi for metered connectors

Thorough, well-tested PR. The two "shape decisions" (upstream errors resolve; query always forwarded) are correct and the tests pin both. Below is feedback organized by category.

Code quality — 👍

  • proxyCall is cleanly factored out and documented; the @internal JSDoc explaining why upstream 4xx resolves rather than throws is exactly the kind of comment that earns its keep (src/modules/connectors.ts:139-147).
  • New assertNonEmptyString helper reduces the repeated validation boilerplate. Nice.
  • camelCase mapping and the raw envelope type (ConnectorProxyRawResponse) match the codebase convention of casting the interceptor's unwrapped response.data via as unknown as — consistent with the sibling methods.

Potential bugs / issues

  1. method not validated / arbitrary strings pass through. request.method is typed as a union, but at runtime any string is accepted and uppercased (connectors.ts:159). The test even passes "post" lowercase. That's fine for the happy path, but an invalid method (e.g. "FETCH") is silently forwarded to the server. Minor — server will reject — but worth deciding if the SDK should guard.
  2. body on GET/HEAD/DELETE. The type doc says body is "Ignored for GET, HEAD, DELETE," but the client always forwards body: request.body ?? null (connectors.ts:163). So the server must be the one ignoring it. That's a reasonable division, but the doc phrasing ("Ignored") reads as if the SDK drops it. Consider "Ignored by the server for GET/HEAD/DELETE" to match actual behavior.
  3. Validation-order asymmetry. callApi validates integrationType before request is even checked for being an object; proxyCall then validates request and path. Given the test.each with ["x", ""] expects the path error, this ordering works, but the two-stage validation split across two functions is a little subtle. Not a bug — just flag for maintainers.

Performance — 👍

No concerns. Single POST per call; no extra allocations of note. The docs' warning about expensive calls inside loops is the right place to address the real cost lever, since it's server-priced.

Security

  • The design (token never leaves the server for metered connectors) is sound and is the whole point.
  • headers passthrough: the type doc says only connector-allowlisted headers are forwarded and the rest dropped server-side (connectors.types.ts:66). Good that the allowlist lives on the server rather than the client. No client-side secret handling here. ✅
  • No logging of request bodies/headers added in this path (the iframe postMessage logging in axios-client.ts is pre-existing and unrelated).

Test coverage — strong

9 tests cover: normalization, GET default, query forwarding, camelCase mapping, upstream-error-resolves, Base44-error-rejects (402), metered 403 on getConnection, and empty identifier/path. A couple of gaps worth considering (optional):

  • No test for missing/non-object requestproxyCall's "Request is required and must be an object" branch (connectors.ts:153-155) is uncovered. Easy to add: callApi("x", undefined as any).
  • No test asserting status/headers/creditsCharged defaults when the server omits them (the ?? null / ?? {} / ?? 0 fallbacks at connectors.ts:169-172). A response missing those fields would exercise the nullish-coalescing paths.
  • method uppercasing is implicitly tested via "post""POST", good.

Minor nits

  • getConnection/getWorkspaceConnection/getCurrentAppUserConnection still use inline if (!x || typeof x !== "string") checks while callApi uses the new assertNonEmptyString. Not required for this PR, but a follow-up could unify them.
  • ConnectorApiResponse.data is typed T but the proxy could return null/error bodies on success: false; callers relying on T without checking success may get a surprise. The docs cover this, so it's acceptable.

Overall: LGTM. The billing-bypass reasoning behind always forwarding query is well-justified, and error semantics are correct and tested. The only thing I'd actually add before merge is a test for the non-object request branch; everything else is optional polish.
• Branch: connectors-money

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/sdk@0.8.43-pr.256.e07d4b3

Prefer not to change any import paths? Install using npm alias so your code still imports @base44/sdk:

npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.43-pr.256.e07d4b3"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "@base44/sdk": "npm:@base44-preview/sdk@0.8.43-pr.256.e07d4b3"
  }
}

Preview published to npm registry — try new features instantly!

ChenMachBase and others added 2 commits August 18, 2026 16:23
The proxy grew two capabilities after this module was written, and `proxyCall`
builds its payload field by field — so `host` was dropped on the floor even when
a caller passed it, and a binary response arrived with `data: null` and no way to
reach the bytes.

- `host` is forwarded when set and omitted when not, so the proxy applies the
  connector's declared default rather than receiving an explicit null.
- `dataBase64` / `contentType` are mapped from the wire, for the media types a
  connector declares as binary. Set instead of `data`, never alongside it.
- `body` no longer claims to be ignored for DELETE; the proxy forwards it, and
  several provider APIs require it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the docs-draft PR has auto-drafted documentation suggestions label Aug 25, 2026
@ChenMachBase
ChenMachBase merged commit 141f521 into main Aug 26, 2026
6 of 8 checks passed
ChenMachBase added a commit that referenced this pull request Aug 26, 2026
- Percent-encode caller-supplied integration types and connector IDs in
  request URLs, so a runtime-built identifier can only select a
  connector, never re-target another route under the privileged token
- Type ConnectorApiResponse.data as T | null to match the documented
  and implemented contract (null for binary and proxy-error responses)
- Treat host: null like undefined when omitting the host field, since
  untyped callers write `host: x ?? null`
- Add nock.disableNetConnect() to the proxy tests so interceptor
  mismatches fail fast instead of escaping as real network requests
- Run test:types in CI so the type-contract tests are enforced

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-draft PR has auto-drafted documentation suggestions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants